/*
  This file is part of JCM.

  JCM is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.

  JCM is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with JCM.  If not, see <http://www.gnu.org/licenses/>.
*/
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.table.*;
import java.awt.*;
import java.awt.event.*;
import java.sql.*;
import java.text.*;
import java.util.*;

/**
 * Diálogo para edição de tabela do banco de dados de índices de correção
 * monetária.
*/
public class EditWindow extends JDialog implements ActionListener, TableModelListener, ListSelectionListener
{
  /**
   * Constrói o diálogo modal configurando o funcionamento da edição.
   *
   * @param owner Frame proprietária desse diálogo.
   * @param connection Conexão aberta ao banco de dados.
   * @param tableName Nome da tabela a editar.
   * @param dbProperties Propriedades específicas do DB.
  */
  public EditWindow(Frame owner, Connection connection, String tableName, Properties dbProperties)
  {
    // invoca a superclasse para criar o diálogo modal com título
    super(owner, String.format("Editando %s.%S", dbProperties.getProperty("dbCatalogName"), tableName), true);

    this.connection = connection;
    this.tableName = tableName;
    this.dbProperties = dbProperties;

    useSQLite = dbProperties.getProperty("protocol").contains("sqlite");

    datum = new ArrayList<Object[]>();
    DateFormat f = new SimpleDateFormat("yyyy-MM-dd");
    String q = String.format(dbProperties.getProperty("all"), tableName);
    Statement stm = null;
    ResultSet result = null;
    try {
      stm = connection.createStatement();
      result = stm.executeQuery(q);
      // loop de montagem da lista de arrays correspondentes aos registros
      while (result.next())
      {
        Object[] row = new Object[2];
        row[0] = f.parse(result.getString(1));
        row[1] = result.getDouble(2);
        datum.add(row);
      }
    } catch (ParseException e) {
      System.err.println(e);
    } catch (SQLException e) {
      System.err.println(e);
    } finally {
      try {
        result.close();
        stm.close();
      } catch (SQLException se) {
        System.err.println(se);
      }
    }
    datum.trimToSize();

    // instancia o table model
    tableModel = new CustomTableModel();
    // adiciona observador de modificações na tabela
    tableModel.addTableModelListener(this);

    // instancia um table usando o table model préviamente preparado
    table = new JTable(tableModel) {
      /**
       * Cria um table header que fornece textos de ajuda contextual
       * para os headers das colunas.
       *
       * @return Componente JTableHeader especializado.
      */
      @Override protected JTableHeader createDefaultTableHeader()
      {
        return new JTableHeader(columnModel) {
          /**
           * Obtêm o texto de ajuda contextual a ser exibido quando o mouse
           * paira sobre um header da tabela.
           *
           * @param e Evento de mouse pairando sobre um header da tabela.
          */
          @Override public String getToolTipText(MouseEvent e)
          {
            String[] headerToolTips = {
              "datas no formato dia/mês/ano", "taxas mensais de correção monetária"
            };
            // obtêm o índice do modêlo de coluna
            int ndx = columnModel.getColumnIndexAtX(e.getPoint().x);
            // obtêm o índice real da coluna
            ndx = columnModel.getColumn(ndx).getModelIndex();
            return headerToolTips[ndx];
          }
          private static final long serialVersionUID = -6921792200568000158L;
        };
      }
      private static final long serialVersionUID = 5317136280874641962L;
    };

    // adiciona observador de seleção de linhas
    table.getSelectionModel().addListSelectionListener(this);

    // declara a dimensão preferida da área de visualização da tabela
    // usando a métrica da fonte padrão no seu tamanho corrente
    Font font = UIManager.getFont("FormattedTextField.font");
    FontMetrics metric = getFontMetrics(font);
    // largura da coluna expandida de 10% para folga visual
    int w = 11 * metric.stringWidth("00/00/0000") / 10;
    // altura proporcional a largura via constante da secção áurea
    int h = (int) (w * (1 + Math.sqrt(5d)) / 2);
    table.setPreferredScrollableViewportSize(new Dimension(2*w, 2*h));

    // declara a altura das linhas para melhor visualização impondo
    // espaçamento extra entre conteúdo e bordas horizontais
    table.setRowHeight(metric.getMaxAscent() + metric.getMaxDescent() + metric.getAscent() / 4);

    if (! System.getProperty("java.version").startsWith("1.5")) {
      // disponível a partir do Java 1.6
      table.setFillsViewportHeight(true);
    }

    // renderização e edição especializadas das células de tipo Date
    table.setDefaultRenderer(java.util.Date.class, new DateRenderer());
    @SuppressWarnings("deprecation")
    java.util.Date minDate = new java.util.Date(0, Calendar.JANUARY, 1);
    @SuppressWarnings("deprecation")
    java.util.Date maxDate = new java.util.Date(199, Calendar.DECEMBER, 31);
    table.setDefaultEditor(java.util.Date.class, new DateEditor(minDate, maxDate));

    // renderização e edição especializadas das células de tipo Double
    table.setDefaultRenderer(Double.class, new DoubleRenderer());
    table.setDefaultEditor(Double.class, new DoubleEditor(-1d, 1d));

    // painel dos botões

    newBtn = buildButton("images/add01.png", "images/add02.png", "<html><center>adiciona novo registro no final da tabela<br>ou após o último selecionado</center></html>", true);

    eraseBtn = buildButton("images/remove01.png", "images/remove02.png", "apaga o(s) registro(s) selecionado(s)", false);

    closeBtn = buildButton("images/exit01.png", "images/exit02.png", "encerra edição", true);

    JToolBar toolBar = new JToolBar();
    toolBar.setMargin(new Insets(0, 0, 0, 0));
    toolBar.setRollover(true);
    toolBar.add(newBtn);
    toolBar.add(eraseBtn);
    // espaçador flexível para posicionar o último botão na extrema direita
    Component glue = Box.createHorizontalGlue();
    glue.setFocusable(false);
    toolBar.add(glue);
    // espaçador usado quando o toolbar segue orientação vertical
    toolBar.addSeparator();
    toolBar.add(closeBtn);

    // painel da barra de status

    font = font.deriveFont((5 * font.getSize2D()) / 6f);

    sizeLabel = new JLabel();
    sizeLabel.setFont(font);

    counterLabel = new JLabel();
    counterLabel.setFont(font.deriveFont(Font.ITALIC));

    JPanel statusPane = new JPanel();
    statusPane.setFont(font.deriveFont(11f));
    statusPane.setBackground(Color.WHITE);
    statusPane.setBorder(BorderFactory.createEmptyBorder(1, 2, 0, 2));
    statusPane.setLayout(new BoxLayout(statusPane, BoxLayout.X_AXIS));
    statusPane.add(sizeLabel);
    statusPane.add(Box.createGlue());
    statusPane.add(counterLabel);

    JPanel pane = new JPanel();
    pane.setLayout(new BorderLayout());
    pane.add(toolBar, BorderLayout.NORTH);
    pane.add(new JScrollPane(table), BorderLayout.CENTER);

    add(pane, BorderLayout.CENTER);
    add(statusPane, BorderLayout.SOUTH);

    // observador do evento window closed para liberar recursos utilizados
    addWindowListener(new WindowAdapter() {
      @Override public void windowClosed(WindowEvent we)
      {
        // backward traversal para evitar unshift
        for (int j=datum.size(); --j >= 0;) datum.remove(j);
        // desmonta a pilha de linhas pendentes
        heap.clear();
      }
    });

    // prepara a pilha de registros pendentes
    heap = new HashSet<Object[]>();

    // atualização inicial da barra de status
    updateStatus();

    // escorre para o fim da tabela
    scrollToTarget();
  }

  /**
   * Monta botão conforme especificações e adiciona listener de ação.
   *
   * @param icoFilename Nome do arquivo resource do icone default.
   * @param rollIcoFilename Nome do arquivo resource do icone rollover.
   * @param toolTipText Texto de ajuda contextual.
   * @param enabled Habilitação de uso.
  */
  private JButton buildButton(String icoFilename, String rollIcoFilename, String toolTipText, boolean enabled)
  {
    JButton b = new JButton();
    b.setIcon(loadIcon(icoFilename));
    b.setRolloverIcon(loadIcon(rollIcoFilename));
    b.setToolTipText(toolTipText);
    b.setEnabled(enabled);
    b.addActionListener(this);
    return b;
  }

  /**
   * Carrega imagem como resource.
   *
   * @param filename Nome do arquivo de imagem.
  */
  private ImageIcon loadIcon(String filename)
  {
    return new ImageIcon(getClass().getClassLoader().getResource(filename));
  }

  /**
   * Cria um componente JRootPane associando teclas a ações.
   *
   * @return Instância de componente JRootPane especializada.
  */
  @Override protected JRootPane createRootPane()
  {
    JRootPane root = new JRootPane();
    String actionName = "EXIT";
    InputMap inputMap = root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
    KeyStroke keyStroke = KeyStroke.getKeyStroke("control shift ESCAPE");
    inputMap.put(keyStroke, actionName);
    ActionMap actionMap = root.getActionMap();
    actionMap.put(actionName, new AbstractAction() {
      public void actionPerformed(ActionEvent ae) { stop(); }
      private static final long serialVersionUID = -7727201964869006422L;
    });

    actionName = "ADD_RECORD";
    keyStroke = KeyStroke.getKeyStroke("control shift INSERT");
    inputMap.put(keyStroke, actionName);
    actionMap.put(actionName, new AbstractAction() {
      public void actionPerformed(ActionEvent ae) { newBtn.doClick(); }
      private static final long serialVersionUID = 3079299508249874494L;
    });

    actionName = "DEL_RECORD";
    keyStroke = KeyStroke.getKeyStroke("control shift DELETE");
    inputMap.put(keyStroke, actionName);
    actionMap.put(actionName, new AbstractAction() {
      public void actionPerformed(ActionEvent ae) { eraseBtn.doClick(); }
      private static final long serialVersionUID = -7327207852895980389L;
    });

    return root;
  }

  /** Última chance de editar registros pendentes. */
  private void stop()
  {
    // checa se há registros pendentes
    if (! heap.isEmpty())
    {
      Object[] options = {"Continuar", "Descartar"};
      String msg = String.format("<html>Há <b>%d</b> registro(s) para inserção em pendência.<br>Continue a sessão de edição ou<br>descarte a(s) pendência(s).</html>", heap.size());
      // notifica o usuário sobre as pendências
      int answer = JOptionPane.showOptionDialog(getContentPane(),
        msg, "Notificação do Editor", JOptionPane.YES_NO_OPTION,
        JOptionPane.WARNING_MESSAGE, null, options, options[0]);
      // checa se deseja continuar
      if (answer == 0)
      {
        // obtêm o primeiro item da pilha de pendentes
        Object[] row = heap.iterator().next();
        // obtêm o índice desse item na tabela
        int rowIndex = datum.indexOf(row);
        // seleciona a linha correspondente
        table.setRowSelectionInterval(rowIndex, rowIndex);
        // escorre o viewport para que a linha seja visível
        scrollToVisible(rowIndex, 0);
        // cancela o encerramento
        return;
      }
    }
    // Well done!
    setVisible(false);
  }

  /** Atualiza a barra de status. */
  private void updateStatus()
  {
    sizeLabel.setText(String.format("#Registros: %d", datum.size()));
    if (heap.isEmpty()) {
      counterLabel.setVisible(false);
    } else {
      int size = heap.size();
      char ch = (size > 1) ? 's' : ' ';
      counterLabel.setText(String.format("%d Pendente%c", size, ch));
      counterLabel.setVisible(true);
    }
  }

  /**
   * Altera a posição do viewport para visualizar a primeira linha
   * selecionada ou a última linha da tabela se não há linha selecionada.
  */
  private void scrollToTarget()
  {
    EventQueue.invokeLater(new Runnable() {
      public void run()
      {
        // tenta obter a primeira linha selecionada como alvo
        int targetRow = table.getSelectedRow();
        // se não há linha selecionada então usa a última linha da tabela como alvo
        if (targetRow < 0) targetRow = datum.size() - 1;
        scrollToCenter(targetRow, 0);
      }
    });
  }

  /**
   * Escorre o painel de visualização da tabela para que a célula
   * de índices fornecidos seja visível no centro.
   *
   * @param row Índice da linha da célula.
   * @param col Índice da coluna da célula.
  */
  private void scrollToCenter(int row, int col)
  {
    JViewport viewport = (JViewport) table.getParent();
    // retângulo da posição e dimensão (inclusive espaços)
    // da célula relativos a tabela
    Rectangle rect = table.getCellRect(row, col, true);
    // localização do view relativo a tabela
    Rectangle viewRect = viewport.getViewRect();
    // translação da localização da célula relativa ao viewport
    // assumindo que o canto superior esquerdo é (0,0)
    rect.setLocation(rect.x - viewRect.x, rect.y - viewRect.y);
    // calcula a localização do rect se estivesse no centro do viewport
    int centerX = (viewRect.width - rect.width) / 2;
    int centerY = (viewRect.height - rect.height) / 2;
    // transforma a localização do rect por "reflexão" para que o
    // procedimento scrollRectToVisible mova a célula para o centro
    if (rect.x < centerX) centerX = -centerX;
    if (rect.y < centerY) centerY = -centerY;
    rect.translate(centerX, centerY);
    // escorre o retângulo da célula no viewport
    viewport.scrollRectToVisible(rect);
  }

  /**
   * Escorre o painel de visualização da tabela para que a célula
   * de índices fornecidos seja visível.
   *
   * @param row Índice da linha da célula.
   * @param col Índice da coluna da célula.
  */
  private void scrollToVisible(int row, int col)
  {
    JViewport viewport = (JViewport) table.getParent();
    // retângulo da posição e dimensão (inclusive espaços)
    // da célula relativos a tabela
    Rectangle rect = table.getCellRect(row, col, true);
    // localização do viewport relativo a tabela
    Point pt = viewport.getViewPosition();
    // translação da localização da célula relativa ao viewport
    // assumindo que o canto superior esquerdo é (0,0)
    rect.setLocation(rect.x - pt.x, rect.y - pt.y);
    // escorre o retângulo da célula no viewport
    viewport.scrollRectToVisible(rect);
  }

  /**
   * Monta instrução SQL preparada conforme nome da expressão.
   *
   * @param task Nome da expressão.
   * @return Instrução SQL preparada
   *
   * @throws Excessão SQL.
  */
  private PreparedStatement buildStatement(String task) throws SQLException
  {
    PreparedStatement ps = null;
    String s = dbProperties.getProperty(task);
    if (s != null) {
      String q = String.format(s, tableName);
      ps = connection.prepareStatement(q);
    }
    return ps;
  }

  /**
   * Executa ação associada ao botão acionado.
   *
   * @param ae O evento de ação observado.
  */
  public void actionPerformed(ActionEvent ae)
  {
    Object source = ae.getSource();
    if (source == newBtn) {

      /*
         Adiciona um novo registro após o último selecionado ou
         ao final da tabela, selecionando a linha correspondente
         e alterando o viewport para sua visualização para
         finalmente entrar em modo de edição na primeira célula
         dessa linha.
      */

      // obtêm o array dos índices das linhas selecionadas
      int[] rows = table.getSelectedRows();
      // se não há linha selecionada então usa o número de
      // linhas da tabela como posição base de inserção
      int iPos = (rows.length > 0) ? rows[rows.length-1] + 1 : datum.size();
      // adiciona um array de nulls na posição de inserção
      datum.add(iPos, new Object[2]);
      // adiciona a pendência ao heap
      heap.add(datum.get(iPos));
      // notifica os listeners sobre a inserção ou apêndice
      tableModel.fireTableRowsInserted(iPos, iPos);
      // seleciona a linha adicionada
      table.setRowSelectionInterval(iPos, iPos);
      // altera a posição do viewport
      scrollToTarget();
      // entra em modo de edição
      table.editCellAt(iPos, 0);

    } else if (source == eraseBtn) {

      /*
         Elimina registros selecionados.

         Nota: Não é necessário testar se há linhas selecionadas pois o
               botão proprietário da ação somente é habilitado se for
               observado evento "selecionar linha" finalizado.
      */

      // obtêm o array dos índices de linhas selecionadas
      int[] rows = table.getSelectedRows();
      java.sql.Date date;
      PreparedStatement ps = null;
      try {
        // monta o sql de eliminação para data especificada
        ps = buildStatement("deleteForDate");
        // contador do número de instruções em batch
        int count = 0;
        // loop de remoção dos registros selecionados
        for (int j=rows.length; --j >= 0;)
        {
          Object[] tableRow = datum.get(rows[j]);
          // checa se a linha corresponde a algum registro pendente
          if (heap.contains(tableRow)) {
            heap.remove(tableRow);  // cancela registro pendente
          } else {
            long millis = ((java.util.Date) tableRow[0]).getTime();
            date = new java.sql.Date(millis);
            if (useSQLite) ps.setString(1, date.toString());
            else ps.setDate(1, date);
            // adiciona a transação ao batch
            ps.addBatch();
            count++;
          }
          // remove a linha da estrutura de apresentação visual
          datum.remove(rows[j]);
        }
        // checa o número de instruções em batch
        if (count > 0) {
          // desativa transações individuais
          connection.setAutoCommit(false);
          // executa o batch no modo de transações agrupadas
          ps.executeBatch();
        }
      } catch (SQLException e) {
        System.err.println(e);
      } finally {
        try {
          ps.close();
          // ativa transações individuais
          connection.setAutoCommit(true);
        } catch (SQLException se) {
          System.err.println(se);
        }
      }
      // notifica os listeners sobre as eliminações
      tableModel.fireTableRowsDeleted(rows[0], rows[rows.length-1]);

      // workaround para o bug de "modo edição" indesejável
      // após eliminação de linha(s) pelo teclado
      if (table.isEditing()) {
        int r = table.getEditingRow();
        if (r >= datum.size()) r = datum.size() - 1;
        table.editingCanceled(new ChangeEvent(table));
        table.setRowSelectionInterval(r, r);
      }

    } else { /* Encerra a sessão de edição. */

      stop();

    }
  }

  /**
   * Observador de eventos associados a modificações dos dados para
   * atualizar e/ou inserir registros diretamente no banco de dados.
   *
   * @param me O evento de modificação dos dados.
  */
  public void tableChanged(TableModelEvent me)
  {
    if (me.getType() == TableModelEvent.UPDATE)
    {
      // obtêm a linha da planilha onde ocorreu o evento
      int rowIndex = me.getLastRow();
      Object[] row = datum.get(rowIndex);

      // checa se o array é item da pilha de pendências
      if (heap.contains(row)) {

        /* Inserção de registro no DB */

        // checa se o array está completamente preenchido
        if ((row[0] != null) && (row[1] != null))
        {
          long millis = ((java.util.Date) row[0]).getTime();
          java.sql.Date date = new java.sql.Date(millis);
          PreparedStatement ps = null;
          try {
            // monta o sql de inserção
            ps = buildStatement("insert");
            if (useSQLite) ps.setString(1, date.toString());
            else ps.setDate(1, date);
            ps.setDouble(2, (Double) row[1]);
            ps.executeUpdate();
            // remove da pilha de registros pendentes
            // a menos que ocorra excessão
            heap.remove(row);
          } catch (SQLException e) {
            JOptionPane.showMessageDialog(getContentPane(),
              "<html>Operação Cancelada.<br>A inserção não é possível pois<br>já existe registro com essa data.</html>", "Notificação do Editor", JOptionPane.ERROR_MESSAGE);
          } finally {
            try {
              ps.close();
            } catch (SQLException se) {
              System.err.println(se);
            }
          }
        }

        // somente para atualizar o estilo da fonte na linha inserida
        // evidenciando o status de pendência
        table.repaint();

      } else {

        /* Atualização de registro no DB */

        // obtêm a coluna da planilha onde ocorreu o evento
        int colIndex = me.getColumn();

        // checa se é atualização de data
        if (colIndex == 0) {

          /* Atualização do campo data */

          // obtêm os milisegundos da data na linha editada
          long millis = ((java.util.Date) row[0]).getTime();

          // checa se houve atualização "de facto"
          if (millis != lastTime)
          {
            // prepara as datas SQL
            java.sql.Date to = new java.sql.Date(millis);
            java.sql.Date from = new java.sql.Date(lastTime);
            PreparedStatement ps = null;
            try {
              // monta o sql para atualização de data
              ps = buildStatement("updateDate");
              if (useSQLite) {
                ps.setString(1, to.toString());
                ps.setString(2, from.toString());
              } else {
                ps.setDate(1, to);
                ps.setDate(2, from);
              }
              ps.executeUpdate();
              // preserva visando edição de demais colunas na mesma linha
              // a menos que ocorra uma exceção
              lastTime = millis;
            } catch (SQLException e) {
              //System.err.println(e);
              JOptionPane.showMessageDialog(getContentPane(),
                "<html>Operação Cancelada.<br>A atualização não é possível pois<br>já existe registro com essa data.</html>", "Notificação do Editor", JOptionPane.ERROR_MESSAGE);
              // restaura a linha da tabela
              ((java.util.Date) row[0]).setTime(lastTime);
              datum.set(rowIndex, row);
            } finally {
              try {
                ps.close();
              } catch (SQLException se) {
                System.err.println(se);
              }
            }
          }

        } else {

          /* Atualização do campo valor */

          // checa se houve atualização "de facto"
          if (((Double) row[1]).doubleValue() != lastValue)
          {
            java.sql.Date date = new java.sql.Date(lastTime);
            PreparedStatement ps = null;
            try {
              // monta o sql de atualização de valor
              ps = buildStatement("updateValue");
              ps.setDouble(1, (Double) row[1]);
              if (useSQLite) ps.setString(2, date.toString());
              else ps.setDate(2, date);
              ps.executeUpdate();
            } catch (SQLException e) {
              System.err.println(e);
            } finally {
              try {
                ps.close();
              } catch (SQLException se) {
                System.err.println(se);
              }
            }
          }

        }

      }
    }

    // atualiza o conteúdo do status bar
    updateStatus();
  }

  /**
   * Observador de eventos de seleção de linhas da tabela habilitando ou
   * desabilitando o botão Apagar.
   *
   * @param se O evento de seleção de linhas.
  */
  public void valueChanged(ListSelectionEvent se)
  {
    // testa se a seleção de linhas está finalizada
    if (! se.getValueIsAdjusting())
    {
      int n = table.getSelectedRowCount();
      // habilita o botão "apagar" conforme número de selecionados
      eraseBtn.setEnabled(n > 0);
      // checa se há única linha selecionada como ocorre em atualizações
      if (n == 1)
      {
        Object[] row = datum.get(table.getSelectedRow());
        // checa se a linha selecionada não corresponde a registro pendente
        if (! heap.contains(row))
        {
          // preserva a data na linha selecionada visando atualização
          lastTime = ((java.util.Date) row[0]).getTime();
          // preserva o valor na linha selecionada visando atualização
          lastValue = ((Double) row[1]).doubleValue();
        }
      }
    }
  }

  /** Classe de renderização das células do tipo Date. */
  private class DateRenderer extends DefaultTableCellRenderer
  {
    private DateFormat df;

    public DateRenderer()
    {
      super();
      // centraliza o texto na coluna
      setHorizontalAlignment(SwingConstants.CENTER);
      df = DateFormat.getDateInstance(DateFormat.MEDIUM, NumberFormatFactory.getLocaleBR());
    }

    @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column)
    {
      final Component c = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
      // checa se a célula pertence a uma linha de registro pendente
      if (heap.contains(datum.get(row))) {
        // alteração "on the fly" do estilo da fonte para
        // evidenciar o status de pendência da linha da tabela
        c.setFont(c.getFont().deriveFont(Font.ITALIC));
      }
      return c;
    }

    @Override public void setValue(Object value)
    {
      if (value == null) setText("");
      else {
        @SuppressWarnings("deprecation")
        int day = ((java.util.Date) value).getDate();
        // alterna a côr do texto para advertir sobre o dia errado
        setForeground((day == 1) ? Color.BLACK : MARROM);
        setText(df.format(value));
      }
    }

    private static final long serialVersionUID = -1547698036031570204L;
  }

  /** Classe de renderização das células de valores numéricos. */
  private class DoubleRenderer extends DefaultTableCellRenderer
  {
    private NumberFormat nf;

    public DoubleRenderer()
    {
      super();
      setHorizontalAlignment(SwingConstants.RIGHT);
      nf = NumberFormatFactory.getDecimalFormat("0.0000##");
    }

    @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column)
    {
      final Component c = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
      // checa se a célula pertence a uma linha de registro pendente
      if (heap.contains(datum.get(row))) {
        // alteração "on the fly" do estilo da fonte para
        // evidenciar o status de pendência da linha da tabela
        c.setFont(c.getFont().deriveFont(Font.ITALIC));
      }
      return c;
    }

    @Override public void setValue(Object value)
    {
      if (value == null) setText("");
      else {
        // alterna a côr do texto para evidenciar o sinal do número
        setForeground(((Number) value).doubleValue() >= 0 ? Color.BLACK : MARROM);
        setText(nf.format(value));
      }
    }

    private static final long serialVersionUID = 3649235752918533334L;
  }

  /** Classe gestora da apresentação dos dados da tabela. */
  private class CustomTableModel extends AbstractTableModel
  {
    String[] columnNames = { "Data", "Valor" };

    /**
     * Retorna o número de colunas/campos da tabela.
     *
     * @return Número de colunas/campos da tabela.
    */
    public int getColumnCount()
    {
      return 2;
    }

    /**
     * Retorna o número de linhas/registros da tabela.
     *
     * @return Número de linhas/registros da tabela.
    */
    public int getRowCount()
    {
      return datum.size();
    }

    /**
     * Retorna o nome da coluna/campo de ordem fornecida.
     *
     * @param Número de ordem da coluna/campo.
     * @return Nome da coluna/campo de ordem fornecida.
    */
    @Override public String getColumnName(int col)
    {
      return columnNames[col];
    }

    /**
     * Retorna o nome da classe do objeto associado a coluna/campo de
     * ordem fornecida.
     *
     * @param Número de ordem da coluna/campo.
     * @return Nome da classe do objeto associado a coluna/campo de
     * ordem fornecida.
    */
    @Override public Class<?> getColumnClass(int col)
    {
      return (col == 0) ? java.util.Date.class : Double.class;
    }

    /**
     * Checa se a célula na linha e coluna fornecida é editável.
     *
     * @param row Número da linha.
     * @param col Número da coluna.
     * @return Permissão de edição associda a célula.
    */
    @Override public boolean isCellEditable(int row, int col)
    {
      return true;
    }

    /**
     * Acessa o valor da célula na linha e coluna fornecida.
     *
     * @param row Número da linha.
     * @param col Número da coluna.
     * @return Objeto associado à célula.
    */
    public Object getValueAt(int row, int col)
    {
      return datum.get(row)[col];
    }

    /**
     * Associa objeto à célula na linha e coluna fornecida.
     *
     * @param value Objeto a ser associado à célula.
     * @param row Número da linha.
     * @param col Número da coluna.
    */
    @Override public void setValueAt(Object value, int row, int col)
    {
      Object[] array = datum.get(row);
      array[col] = value;
      datum.set(row, array);
      fireTableCellUpdated(row, col);
    }

    private static final long serialVersionUID = -6970271420533522360L;
  }

  /** Instância do gestor de dados da tabela. */
  private CustomTableModel tableModel;

  /** Lista de arrays correspondentes a registros da tabela. */
  private ArrayList<Object[]> datum;

  /** Lista de arrays correspondentes a registros em inserção pendente */
  private HashSet<Object[]> heap;

  /** Milisegundos da data da linha selecionada mais recentemente. */
  private long lastTime;

  /** Valor da linha selecionada mais recentemente. */
  private double lastValue;

  private JTable table;

  private JButton newBtn;

  private JButton eraseBtn;

  private JButton closeBtn;

  /** Label no status bar para exibir o número de linhas da tabela. */
  private JLabel sizeLabel;

  /** Label no status bar para exibir o número de linhas pendentes. */
  private JLabel counterLabel;

  /** Conexão emprestada pelo aplicativo proprietário. */
  private Connection connection;

  /** Propriedades do banco de dados servido pela conexão. */
  private Properties dbProperties;

  /** Nome da tabela do banco de dados cujo conteúdo é editado. */
  private String tableName;

  /** Indicador de uso do SQLite. */
  private boolean useSQLite;

  private final Color MARROM = new Color(150, 0, 0);

  private static final long serialVersionUID = 923916801534276078L;
}
